/*
 * Copyright 2017 Matthew Tamlin
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.matthewtamlin.fortytwo.library.answer_group;

import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.components.DirectionalLayout;
import ohos.app.Context;

import com.matthewtamlin.fortytwo.library.answer_view.AnswerView;
import com.matthewtamlin.fortytwo.library.util.EvictingStackSet;
import com.matthewtamlin.fortytwo.library.util.EvictingStackSet.EvictionListener;
import com.matthewtamlin.java_utilities.checkers.IntChecker;
import com.matthewtamlin.java_utilities.checkers.NullChecker;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * An AnswerGroup which limits the number of view which can be selected at any given time. The limit
 * can be set at any time, and is automatically enforced when views are clicked. If the limit has
 * been reached and a view is clicked, the view which was least recently selected will be deselected
 * to allow for the newly selected view.
 */
public class SelectionLimitedAnswerGroup extends DirectionalLayout implements AnswerGroup {
    /**
     * The listeners which have registered for callbacks.
     */
    private final Set<Listener> listeners = new HashSet<>();

    /**
     * All answers which are currently displayed in this group.
     */
    private final List<AnswerView> allAnswers = new ArrayList<>();

    /**
     * All answers which are currently displayed and selected. The size of the stack enforces the
     * selection limit.
     */
    private final EvictingStackSet<AnswerView> selectedViews = new EvictingStackSet<>(1);

    /**
     * Listens to eviction callbacks from the {@code selectedViews} and selects the evicted view.
     */
    private final EvictionListener<AnswerView> evictionListener =
        new EvictionListener<AnswerView>() {
            @Override
            public void onEviction(final EvictingStackSet<AnswerView> evictingStackSet,
                                   final AnswerView evicted) {
                deselectView(evicted);
            }
        };

    /**
     * Whether or not the selection status of marked views can be changed.
     */
    private boolean allowSelectionChangesWhenMarked = false;

    /**
     * Whether or not animations should be shown when selecting and deselecting views.
     */
    private boolean selectionAnimationsEnabled = true;

    /**
     * Constructs a new SelectionLimitAnswerGroup. The selection limit is initially set to 1.
     *
     * @param context the context the view is operating in, not null
     */
    public SelectionLimitedAnswerGroup(final Context context) {
        super(context);
        init();
    }

    /**
     * Constructs a new SelectionLimitAnswerGroup. The selection limit is initially set to 1.
     *
     * @param context the context this view is operating in, not null
     * @param attrs configuration attributes, null allowed
     */
    public SelectionLimitedAnswerGroup(final Context context, final AttrSet attrs) {
        super(context, attrs);
        init();
    }

    /**
     * Constructs a new SelectionLimitAnswerGroup. The selection limit is initially set to 1.
     *
     * @param context the context this view is operating in, not null
     * @param attrs configuration attributes, null allowed
     * @param defStyleAttr an attribute in the current theme which supplies default attributes, pass 0	to ignore
     */
    public SelectionLimitedAnswerGroup(final Context context, final AttrSet attrs,
                                       final String defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    /**
     * Sets the number of answer views which can be selected at any time. If the new limit is less
     * than the current number of selected views, the least recently selected views are deselected
     * so that the number of selected views equals the limit.
     *
     * @param limit the new limit, greater than zero
     * @throws IllegalArgumentException if {@code limit} is less than 1
     */
    public void setMultipleSelectionLimit(final int limit) {
        IntChecker.checkGreaterThan(limit, 0, "limit cannot be less than 1.");
        selectedViews.setMaxSize(limit);
    }

    /**
     * Enables/disables animations when answer views are selected/deselected.
     *
     * @param enable true to enable animations, false to disable them
     */
    public void enableSelectionAnimations(final boolean enable) {
        selectionAnimationsEnabled = enable;
    }

    /**
     * Whether or not animations are enabled when views are selected/deselected
     *
     * @return true if animations are enabled, false otherwise
     */
    public boolean selectionAnimationsAreEnabled() {
        return selectionAnimationsEnabled;
    }

    /**
     * getMultipleSelectionLimit
     *
     * @return the current selection limit of this group
     */
    public int getMultipleSelectionLimit() {
        return selectedViews.getMaxSize();
    }

    @Override
    public void addAnswers(final Collection<? extends AnswerView> answers) {
        NullChecker.checkEachElementIsNotNull(answers, "answers cannot be null or contain null.");

        allAnswers.addAll(answers);

        for (final AnswerView answer : answers) {
            addComponent((Component) answer);

            ((Component) answer).setClickedListener(new ClickedListener() {
                @Override
                public void onClick(Component component) {
                    handleClick(answer);
                }
            });

            if (answer.isSelected()) {
                selectedViews.add(answer);
            }
        }
    }

    @Override
    public void addAnswer(final AnswerView answer) {
        NullChecker.checkNotNull(answer, "answer cannot be null.");

        final List<AnswerView> answers = new ArrayList<>();
        answers.add(answer);

        addAnswers(answers);
    }

    @Override
    public void removeAnswer(final AnswerView answer) {
        NullChecker.checkNotNull(answer, "answer cannot be null.");

        allAnswers.remove(answer);
        selectedViews.remove(answer);

        removeComponent((Component) answer);
        ((Component) answer).setClickedListener(null);
    }

    @Override
    public void clearAnswers() {
        // Use a copy to avoid concurrent modification exceptions when removeAnswer is called
        final List<AnswerView> allAnswersCopy = new ArrayList<>(allAnswers);

        for (final AnswerView answer : allAnswersCopy) {
            removeAnswer(answer);
        }
    }

    @Override
    public List<AnswerView> getAnswers() {
        return new ArrayList<>(allAnswers);
    }

    @Override
    public void allowSelectionChangesWhenMarked(final boolean allow) {
        allowSelectionChangesWhenMarked = allow;
    }

    @Override
    public boolean selectionChangesAreAllowedWhenMarked() {
        return allowSelectionChangesWhenMarked;
    }

    @Override
    public void declareExternalViewSelectionChanges() {
        selectedViews.clear();

        for (final AnswerView answer : allAnswers) {
            if (answer.isSelected()) {
                selectedViews.add(answer);
            }
        }
    }

    @Override
    public void registerListener(final Listener listener) {
        if (listener != null) {
            listeners.add(listener);
        }
    }

    @Override
    public void unregisterListener(final Listener listener) {
        listeners.remove(listener);
    }

    /**
     * Common initializer method for this view. This method should only be called from a
     * constructor.
     */
    private void init() {
        setOrientation(VERTICAL);

        selectedViews.registerListener(evictionListener);
    }

    /**
     * Handles clicks on answer views contained within this group.
     *
     * @param clickedView the answer view which was clicked, not null
     */
    private void handleClick(final AnswerView clickedView) {
        boolean allowSelectionChange = !(clickedView.isMarked()
            && !allowSelectionChangesWhenMarked);

        if (allowSelectionChange) {
            if (clickedView.isSelected()) {
                deselectView(clickedView);
            } else {
                selectView(clickedView);
            }
        }
    }

    /**
     * Deselects the supplied view and calls any registered listeners. Calling this method with a
     * view which is already deselected causes the method to exit immediately.
     *
     * @param answerView the view to deselect, not null
     */
    private void deselectView(final AnswerView answerView) {
        if (answerView.isSelected()) {
            answerView.setSelectedStatus(false, selectionAnimationsEnabled);

            selectedViews.remove(answerView);

            for (final Listener listener : listeners) {
                listener.onAnswerDeselected(this, answerView);
            }
        }
    }

    /**
     * Selects the supplied view and calls any registered listeners. Calling this method with a view
     * which is already selected causes the method to exit immediately.
     *
     * @param answerView the view to select, not null
     */
    private void selectView(final AnswerView answerView) {
        if (!answerView.isSelected()) {
            answerView.setSelectedStatus(true, selectionAnimationsEnabled);

            selectedViews.push(answerView);

            for (final Listener listener : listeners) {
                listener.onAnswerSelected(this, answerView);
            }
        }
    }
}